﻿using System;
using System.Collections.Generic;
using Sustainalytics.Admin.Synchronization.Utility;
using Sustainalytics.DataLayer;
using Sustainalytics.Entities;
using Sustainalytics.Utils;
using System.Linq;
using System.Security.Claims;
using Sustainalytics.Events.Exceptions;
using Sustainalytics.Replication.Publisher;
using Newtonsoft.Json;
using MongoDB.Driver;
using System.Linq.Expressions;
using MongoDB.Bson;
using System.Text.RegularExpressions;

namespace Sustainalytics.Events
{
    public class EventsManagement : IEventsManagement
    {
        private readonly ServiceBusPublisher _serviceBusPublisher;
        private readonly IMongoCollection<Event> _eventCollection;

        public EventsManagement(
            ServiceBusPublisher serviceBusPublisher,
            IMongoCollection<Event> eventCollection)
        {
            _serviceBusPublisher = serviceBusPublisher;
            _eventCollection = eventCollection;
        }

        private EventsManagement(IMongoCollection<Event> eventCollection, ServiceBusPublisher serviceBusPublisher)
        {
            _eventCollection = eventCollection;
            _serviceBusPublisher = serviceBusPublisher;
        }

        public static IEventsManagement GetInstance(IMongoCollection<Event> eventCollection, ServiceBusPublisher serviceBusPublisher)
        {
            IEventsManagement eventsManagement = new EventsManagement(eventCollection, serviceBusPublisher);
            return eventsManagement;
        }

        public List<Event> GetEvents(EventsFilter filter, int skip, int take, string[] orderBy, bool orderAsc)
        {
            var listOptions = new
            {
                filter = filter,
                skip = skip,
                take = take,
                orderBy = orderBy,
                orderAsc = orderAsc
            };

            try
            {
                Guard.ArgumentGreaterOrEqualThan(0, skip, "skip");
                Guard.ArgumentGreaterOrEqualThan(0, take, "take");

                List<FilterDefinition<Event>> filters = GetMongoFilters(filter);
                var orderByString = orderBy != null && orderBy.Any() ? orderBy.First() : "StartDate";
                EventSortOrderDictionary.TryGetValue(orderByString, out Expression<Func<Event, object>> order);

                return _eventCollection
                        .Find(filters.Any() ? 
                            Builders<Event>.Filter.And(filters) : 
                            Builders<Event>.Filter.Empty)
                        .Sort(orderAsc ?
                            Builders<Event>.Sort.Ascending(order) :
                            Builders<Event>.Sort.Descending(order))
                        .Skip(skip)
                        .Limit(take)
                        .ToList();
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        public Event GetEvent(Guid eventId)
        {
            try
            {
                return _eventCollection.Find(x => x.Id == eventId).FirstOrDefault(); 
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        public long GetEventsCount(EventsFilter filter)
        {
            try
            {
                List<FilterDefinition<Event>> filters = GetMongoFilters(filter);

                return _eventCollection.Count(Builders<Event>.Filter.And(filters));
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        public Event AddEvent(Event eventTemplate)
        {
            try
            {
                ValidateEvent(eventTemplate);
                eventTemplate.Id = Guid.NewGuid();

                eventTemplate.AddedByName = ClaimsPrincipal.Current.Claims.Where(s => s.Type == ClaimTypes.GivenName)
                .Select(s => s.Value)
                .SingleOrDefault() + " " + ClaimsPrincipal.Current.Claims.Where(s => s.Type == ClaimTypes.Surname)
                    .Select(s => s.Value)
                    .SingleOrDefault();
                eventTemplate.AddedByUserId = new Guid(ClaimsPrincipal.Current.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier).SingleOrDefault().Value);
                eventTemplate.AddedByNameLowerCase = eventTemplate.AddedByName.ToLowerInvariant();

                _eventCollection.InsertOne(eventTemplate);

                Event addedEvent = _eventCollection.Find(x=> x.Id == eventTemplate.Id).FirstOrDefault();

                //Serialize the newly added event an replicate to anoter region trough Service Bus
                _serviceBusPublisher.SendMessage(JsonConvert.SerializeObject(addedEvent), ReplicationUtils.EntityType.Event, ReplicationUtils.Operation.Create);

                return addedEvent;
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        public Event Update(Guid eventId, EventUpdate patch)
        {
            var arguments = LogArgument.GetString(new {eventId = eventId, patch = patch});
            try
            {
                var existingEvent = _eventCollection.Find(x => x.Id == eventId).FirstOrDefault(); 

                if (existingEvent == null)
                    throw new EventNotFoundException(eventId);

                if(existingEvent.Status != EventStatus.Draft) 
                {
                    throw new UpdatePublishedEventException("An event in published state cannot be updated") { EventId = existingEvent.Id};
                }
                if (patch == null)
                    return existingEvent;
                
                existingEvent.Body = patch.Body;
                existingEvent.EndDate = patch.EndDate;
                existingEvent.Location = patch.Location;
                existingEvent.Name = patch.Name;
                existingEvent.Presenter = patch.Presenter;
                existingEvent.Sponsorship = patch.Sponsorship;
                existingEvent.StartDate = patch.StartDate;
                existingEvent.Status = patch.Status;

                ValidateEvent(existingEvent);

                _eventCollection.ReplaceOne(
                    Builders<Event>.Filter.Eq(x => x.Id, eventId),
                    existingEvent);

                //Serialize the updated event an replicate to anoter region trough Service Bus
                _serviceBusPublisher.SendMessage(JsonConvert.SerializeObject(existingEvent), ReplicationUtils.EntityType.Event, ReplicationUtils.Operation.Update);

                return existingEvent;
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        public void Publish(List<Guid> eventsId)
        {
            var argument = LogArgument.GetString(eventsId);
           
            try
            {
                if (eventsId == null || !eventsId.Any())
                    return;

                _eventCollection.UpdateMany(
                    Builders<Event>.Filter.In(x => x.Id, eventsId),
                    Builders<Event>.Update.Set(x => x.Status, EventStatus.Published));
                    
                var eventsList = _eventCollection.Find(x=> eventsId.Contains(x.Id)).ToList();
                foreach (Event evt in eventsList)
                {
                    evt.Status = EventStatus.Published;
                    //Serialize the updated event an replicate to anoter region trough Service Bus
                    _serviceBusPublisher.SendMessage(JsonConvert.SerializeObject(evt), ReplicationUtils.EntityType.Event, ReplicationUtils.Operation.Update);
                }
            }
            catch (Exception ex)
            {
                throw;
            }            
        }

       
        public void Delete(List<Guid> eventsIds)
        {
            var argument = LogArgument.GetString(eventsIds);

            try
            {
                if (eventsIds == null || eventsIds.Count == 0)
                    return;

                foreach (Guid eventId in eventsIds)
                {
                    //Replicate delete op to anoter region trough Service Bus
                    _serviceBusPublisher.SendMessage(JsonConvert.SerializeObject(eventId), ReplicationUtils.EntityType.Event, ReplicationUtils.Operation.Delete);
                }

                _eventCollection.DeleteMany(x => eventsIds.Contains(x.Id));
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        private void ValidateEvent(Event eventObj)
        {
            var validationResults = new List<ValidationResult>();

            if (eventObj == null)
            {
                throw new EventValidationException().Add(new ValidationResult { Code = EventValidationCodes.NullEvent });
            }

            if (string.IsNullOrEmpty(eventObj.Name))
            {
                validationResults.Add(new ValidationResult { Code = EventValidationCodes.NameNullOrEmpty });
            }
            else
            {
                if (eventObj.Name.Length > 200)
                {
                    validationResults.Add(new ValidationResult { Code = EventValidationCodes.NameLenghtInvalid });
                }
            }

            // body
            if (string.IsNullOrEmpty(eventObj.Body))
            {
                validationResults.Add(new ValidationResult { Code = EventValidationCodes.BodyNullOrEmpty });
            }

            // location
            if (string.IsNullOrEmpty(eventObj.Location))
            {
                validationResults.Add(new ValidationResult { Code = EventValidationCodes.LocationNullOrEmpty });
            }
            else
            {
                if (eventObj.Location.Length > 100)
                {
                    validationResults.Add(new ValidationResult { Code = EventValidationCodes.LocationLenghtInvalid });
                }
            }

            // presenter
            if (!string.IsNullOrEmpty(eventObj.Presenter))
            {
                if (eventObj.Presenter.Length > 100)
                {
                    validationResults.Add(new ValidationResult { Code = EventValidationCodes.PresenterLenghtInvalid });
                }
            }
            
            // overall
            if (validationResults.Count > 0)
            {
                throw new EventValidationException { Results = validationResults };
            }
        }

        private static readonly Dictionary<string, Expression<Func<Event, object>>> EventSortOrderDictionary = new Dictionary<string, Expression<Func<Event, object>>>()
        {
            { "Name",                   x => x.Name                 },
            { "StartDate",              x => x.StartDate            },
            { "EndDate",                x => x.EndDate              },
            { "AddedBy",                x => x.AddedByName          },
            { "AddedByNameLowerCase",   x => x.AddedByNameLowerCase },
            { "Status",                 x => x.Status               },
        };

        private static List<FilterDefinition<Event>> GetMongoFilters(EventsFilter filter)
        {
            var filters = new List<FilterDefinition<Event>>();
            if (filter.Sponsorship.HasValue)
                filters.Add(Builders<Event>.Filter.Eq(x => x.Sponsorship, filter.Sponsorship.Value));

            if (filter != null && !String.IsNullOrWhiteSpace(filter.SearchText))
            {
                var regex = GetSearchEventsRegex(filter.SearchText);
                filters.Add(Builders<Event>.Filter.Or(
                    Builders<Event>.Filter.Regex(x => x.Name, regex),
                    Builders<Event>.Filter.Regex(x => x.Body, regex))
                    );
            }

            if (filter.IsPublished)
            {
                if (filter.StartDateBegin.HasValue && filter.StartDateEnd.HasValue)
                {
                    filters.Add(
                Builders<Event>.Filter.Or(
                    //es <= fs && ee <= fe && ee >= fs && es < fe
                    Builders<Event>.Filter.And(
                        Builders<Event>.Filter.Lte(x => x.StartDate, filter.StartDateBegin),
                        Builders<Event>.Filter.Lte(x => x.EndDate, filter.StartDateEnd),
                        Builders<Event>.Filter.Gte(x => x.EndDate, filter.StartDateBegin),
                        Builders<Event>.Filter.Lt(x => x.StartDate, filter.StartDateEnd),
                        Builders<Event>.Filter.Eq(x => x.Status, EventStatus.Published)
                    ),
                    //es <= fs && ee >= fe && ee >= fs && es < fe
                    Builders<Event>.Filter.And(
                        Builders<Event>.Filter.Lte(x => x.StartDate, filter.StartDateBegin),
                        Builders<Event>.Filter.Gte(x => x.EndDate, filter.StartDateEnd),
                        Builders<Event>.Filter.Gte(x => x.EndDate, filter.StartDateBegin),
                        Builders<Event>.Filter.Lt(x => x.StartDate, filter.StartDateEnd),
                        Builders<Event>.Filter.Eq(x => x.Status, EventStatus.Published)
                    ),
                    //es >= fs && ee <= fe && ee >= fs && es < fe
                    Builders<Event>.Filter.And(
                        Builders<Event>.Filter.Gte(x => x.StartDate, filter.StartDateBegin),
                        Builders<Event>.Filter.Lte(x => x.EndDate, filter.StartDateEnd),
                        Builders<Event>.Filter.Gte(x => x.EndDate, filter.StartDateBegin),
                        Builders<Event>.Filter.Lt(x => x.StartDate, filter.StartDateEnd),
                        Builders<Event>.Filter.Eq(x => x.Status, EventStatus.Published)
                   ),
                   //es >= fs && ee >= fe && ee >= fs && es < fe
                   Builders<Event>.Filter.And(
                        Builders<Event>.Filter.Gte(x => x.StartDate, filter.StartDateBegin),
                        Builders<Event>.Filter.Gte(x => x.EndDate, filter.StartDateEnd),
                        Builders<Event>.Filter.Gte(x => x.EndDate, filter.StartDateBegin),
                        Builders<Event>.Filter.Lt(x => x.StartDate, filter.StartDateEnd),
                        Builders<Event>.Filter.Eq(x => x.Status, EventStatus.Published))
                ));
                }
                else
                {
                    filters.Add(Builders<Event>.Filter.And(
                    Builders<Event>.Filter.Eq(x => x.Status, EventStatus.Published),
                    Builders<Event>.Filter.Gt(x => x.StartDate, DateTime.UtcNow.AddMonths(-3)))
                    );
                }
            }

            if (filters.Count == 0)
                filters.Add(Builders<Event>.Filter.Empty);

            return filters;
        }

        private static BsonRegularExpression GetSearchEventsRegex(string searchText)
        {
            Guard.ArgumentNotNullOrEmpty(searchText, "searchText");

            var text = Regex.Escape(searchText);
            return new BsonRegularExpression(string.Format(".*{0}.*", text), "i");
        }
    }
}
